import multiprocessing
from OpenGL.GL import *

from Constants import CHUNK_HEIGHT, CHUNK_SIZE, GROUND_HEIGHT, WORLD_SIZE, WORLD_SEED, NOISE_SCALE, PROCESS_AMOUNT
from Utils import logger
from MeshData import ChunkMesh
import numpy as np
import Utils
import perlin_noise
from World import Biome, BiomeRegistry, BlockType, BlockRegistry
import random

"""
游戏世界地形生成
"""


def generate_terrain(chunk_position):
    """生成指定区块的地形"""
    terrain = np.zeros((CHUNK_SIZE, CHUNK_HEIGHT, CHUNK_SIZE), dtype=np.int8)

    heightmap = np.zeros((CHUNK_SIZE, CHUNK_SIZE), dtype=np.int32)
    biome_map = np.zeros((CHUNK_SIZE, CHUNK_SIZE), dtype=np.int8)
    buildings = []
    for x in range(0, CHUNK_SIZE):
        for z in range(0, CHUNK_SIZE):
            noise_value = perlin_noise.complex_terrain_noise(
                WORLD_SEED[0] + x + chunk_position[0],
                WORLD_SEED[1] + z + chunk_position[1], NOISE_SCALE
            )
            heightmap[x, z] = noise_value * GROUND_HEIGHT
            # 生物群落的生成
            biome_noise_value = perlin_noise.perlin_noise_2d(
                WORLD_SEED[0] + x + chunk_position[0],
                WORLD_SEED[1] + z + chunk_position[1],
                NOISE_SCALE * 0.5 * 11
            )
            biome_type = Biome.get_biome_by_noise_value(biome_noise_value)
            biome_map[x, z] = biome_type
            # 建筑的生成
            building_noise_value = perlin_noise.perlin_noise_2d(
                WORLD_SEED[0] + x + chunk_position[0] + 666,
                WORLD_SEED[1] + z + chunk_position[1] + 666,
                NOISE_SCALE * 0.1
            )
            # 植物的生成
            plant_noise_value = perlin_noise.perlin_noise_2d(
                WORLD_SEED[0] + x + chunk_position[0] - 666,
                WORLD_SEED[1] + z + chunk_position[1] - 666,
                NOISE_SCALE * 0.1
            )
            if 0.5 < building_noise_value < 0.50025:
                buildings.append((
                    (x + chunk_position[0], int(heightmap[x, z] + 1), z + chunk_position[1]),
                    BiomeRegistry[biome_type].building_type
                ))
            if 0 <= heightmap[x, z] + 1 < CHUNK_HEIGHT:
                if 0.5 < plant_noise_value < 0.5005:
                    terrain[x, heightmap[x, z] + 1, z] = BlockType.Poppy.value
                elif 0.5005 < plant_noise_value < 0.5010:
                    terrain[x, heightmap[x, z] + 1, z] = BlockType.Dandelion.value
                elif 0.4995 < plant_noise_value < 0.5:
                    terrain[x, heightmap[x, z] + 1, z] = BlockType.Allium.value
                elif 0.4990 < plant_noise_value < 0.4995:
                    terrain[x, heightmap[x, z] + 1, z] = BlockType.AzureBluet.value
            # if (0.5 < plant_noise_value < 0.5005 or 0.3 < plant_noise_value < 0.3005) and 0 <= heightmap[x, z] + 1 < CHUNK_HEIGHT:
            #     terrain[x, heightmap[x, z] + 1, z] = BlockType.Poppy.value
    h = heightmap.reshape(CHUNK_SIZE, 1, CHUNK_SIZE)
    b = biome_map.reshape(CHUNK_SIZE, 1, CHUNK_SIZE)
    y_indices = np.arange(CHUNK_HEIGHT).reshape(1, -1, 1)

    for biome_id, biome in BiomeRegistry.items():
        terrain[(y_indices == h) & (b == biome_id)] = biome.grass_layer.value  # 草地层
        dirt_layer_height = random.randint(3, 7)  # 随机泥土层的高度
        terrain[(y_indices >= h - dirt_layer_height) & (y_indices < h) & (b == biome_id)] = biome.dirt_layer.value
        terrain[(y_indices < h - dirt_layer_height) & (b == biome_id)] = biome.stone_layer.value  # 石层

    return terrain, buildings


class Chunk:
    def __init__(self, position, world):  # position = (x, z)
        self.position = position
        self.blocks = np.full((CHUNK_SIZE, CHUNK_HEIGHT, CHUNK_SIZE), dtype=np.int8, fill_value=BlockType.Air.value)
        self.world = world
        self.mesh = ChunkMesh(self)
        self.need_save = False
        self.generate_terrain()

    def check_file(self):
        load_data = self.world.save_manager.load_chunk_data(self.position)
        if load_data is not None:
            self.blocks = load_data
            return True
        return False

    def generate_terrain_callback(self, result):
        self.blocks = result[0]
        self.world.buildings.extend(result[1])
        self.world.chunk_generate_terrain_callback()

    def generate_terrain(self):
        if self.check_file():
            self.world.chunk_generate_terrain_callback()
            return
        self.world.pool.apply_async(generate_terrain, args=(self.position,), callback=self.generate_terrain_callback)

    def generate_mesh(self):
        self.mesh.generate_data()

    def draw(self):
        self.mesh.draw()

    def save(self):
        self.world.save_manager.save_chunk_data(self.position, self.blocks)
        self.need_save = False

    def get_block(self, local_position):
        local_position = (int(local_position[0]), int(local_position[1]), int(local_position[2]))
        if 0 <= local_position[0] < CHUNK_SIZE and 0 <= local_position[1] < CHUNK_HEIGHT and 0 <= local_position[
            2] < CHUNK_SIZE:
            return self.blocks[local_position[0], local_position[1], local_position[2]]
        else:  # 在区块之外
            return self.world.get_block(
                (self.position[0] + local_position[0], local_position[1], self.position[1] + local_position[2]))

    def __del__(self):
        if self.need_save:
            print('del!')
            self.world.chunks_to_save.remove(self)
            self.save()


class World:
    def __init__(self, game_clock, save_manager, opaque_material, shadow_material):
        self.chunks = {}
        self.center_pos = (0, 0)
        self.buildings = []

        self.game_clock, self.save_manager = game_clock, save_manager

        self.opaque_material = opaque_material
        self.shadow_material = shadow_material

        self.pool = multiprocessing.Pool(processes=PROCESS_AMOUNT)

        self.chunks_to_save = set()
        self.loading_chunk_count = 0
        self.draw_count = 0
        self.chunk_key_to_update = set()

    def generate_world(self, center_pos=(0, 0)):
        # 获取需要加载的区块范围
        self.center_pos = center_pos
        min_range = -WORLD_SIZE // 2 + 1  # -1
        max_range = WORLD_SIZE // 2 + 1  # 2

        # 获取需要加载区块的位置
        for x in range(min_range, max_range):
            for z in range(min_range, max_range):
                chunk_pos = (x * CHUNK_SIZE + self.center_pos[0], z * CHUNK_SIZE + self.center_pos[1])
                self.loading_chunk_count += 1
                self.chunk_key_to_update.add(chunk_pos)
                self.chunks[chunk_pos] = Chunk(chunk_pos, self)
        logger.info(f"加载{center_pos}中心的的{max_range}X{max_range}区块的数据")

    def chunk_generate_terrain_callback(self):
        self.loading_chunk_count -= 1
        if self.loading_chunk_count == 0:
            try:
                for building in self.buildings:
                    for block_and_pos in building[1]:
                        self.set_block(block_and_pos[0], (
                            building[0][0] + block_and_pos[1][0],
                            building[0][1] + block_and_pos[1][1],
                            building[0][2] + block_and_pos[1][2]
                        ))
                for chunk_key in list(self.chunk_key_to_update):
                    if chunk_key in self.chunks:
                        self.chunks[chunk_key].generate_mesh()
            except AssertionError:
                print('集合正在变化，但程序尝试修改他')
            except RuntimeError:
                print('集合正在变化，但程序尝试修改他')
            self.buildings = []
            self.chunk_key_to_update = set()

    def generate_more_chunk(self, player_position):
        self.center_pos = Utils.get_chunk_pos_by_world_pos(player_position)
        min_range = -WORLD_SIZE // 2 + 1  # -1
        max_range = WORLD_SIZE // 2 + 1  # 2
        chunk_key_to_keep = []
        for x in range(min_range, max_range):
            for z in range(min_range, max_range):
                chunk_pos = (x * CHUNK_SIZE + self.center_pos[0], z * CHUNK_SIZE + self.center_pos[1])
                chunk_key_to_keep.append(chunk_pos)
                if not chunk_pos in self.chunks:
                    self.loading_chunk_count += 1
                    self.chunks[chunk_pos] = Chunk(chunk_pos, self)
                    self.chunk_key_to_update.add(chunk_pos)
                    self.chunk_key_to_update.add((chunk_pos[0] + CHUNK_SIZE, chunk_pos[1] + CHUNK_SIZE))
                    self.chunk_key_to_update.add((chunk_pos[0] - CHUNK_SIZE, chunk_pos[1] + CHUNK_SIZE))
                    self.chunk_key_to_update.add((chunk_pos[0] + CHUNK_SIZE, chunk_pos[1] - CHUNK_SIZE))
                    self.chunk_key_to_update.add((chunk_pos[0] - CHUNK_SIZE, chunk_pos[1] - CHUNK_SIZE))
        self.chunks = {key: self.chunks[key] for key in chunk_key_to_keep if key in self.chunks}

    def draw_world(self):
        glUseProgram(self.opaque_material.shader)
        # glActiveTexture(GL_TEXTURE0)
        glBindTexture(GL_TEXTURE_2D, self.opaque_material.texture)  # 绑定纹理
        # glActiveTexture(GL_TEXTURE1)
        # glBindTexture(GL_TEXTURE_2D, self.opaque_material.normal)  # 绑定纹理

        # 获取uniform位置并设置纹理单元
        # diffuse_loc = glGetUniformLocation(self.opaque_material.shader, "ourTexture")
        # normal_loc = glGetUniformLocation(self.opaque_material.shader, "normalMap")

        # 告诉着色器每个采样器使用哪个纹理单元
        # glUniform1i(diffuse_loc, 0)  # 对应GL_TEXTURE0
        # glUniform1i(normal_loc, 1)  # 对应GL_TEXTURE1

        for chunk in self.chunks.values():
            chunk.draw()
        # glActiveTexture(GL_TEXTURE0)

    def __in_world_range(self, world_position):
        """是否在世界范围内"""
        chunk_pos = Utils.get_chunk_pos_by_world_pos(world_position)
        return chunk_pos in self.chunks and 0 <= world_position[1] < CHUNK_HEIGHT

    def get_block(self, world_position):
        """获取方块"""
        chunk_pos = Utils.get_chunk_pos_by_world_pos(world_position)
        if self.__in_world_range(world_position):
            local_pos = (world_position[0] - chunk_pos[0], world_position[1], world_position[2] - chunk_pos[1])
            return self.chunks[chunk_pos].get_block(local_pos)
        else:
            return -1

    def get_nearby_block_poses(self, world_position, radius):
        """获取附近的块姿势"""
        x, y, z = world_position[0], world_position[1], world_position[2]
        result = []
        for dx in range(-radius, radius + 1):
            for dy in range(-radius, radius + 1):
                for dz in range(-radius, radius + 1):
                    check_pos = (int(x) + dx, int(y) + dy, int(z) + dz)
                    if block_data := BlockRegistry.get(self.get_block(check_pos)):
                        if block_data.is_solid:
                            result.append(check_pos)
        return result

    def set_block(self, block_type, world_position, is_enum=True):
        """
            设置方块数据。

            参数:
                block_type (BlockType): 方块种类，枚举类型\n
                world_position ((x, y, z)): 世界坐标位置

            返回:
                -1: 放置失败
        """
        chunk_pos = Utils.get_chunk_pos_by_world_pos(world_position)
        if self.__in_world_range(world_position):
            local_pos = (world_position[0] - chunk_pos[0], world_position[1], world_position[2] - chunk_pos[1])
            self.chunks[chunk_pos].blocks[local_pos[0], local_pos[1], local_pos[2]] = block_type if not is_enum else \
                block_type.value
        else:
            return -1

    def player_set_block(self, world_position, block_type, is_enum=True):
        if self.set_block(block_type, Utils.float_position_to_block(world_position), is_enum) != -1:
            chunk_key_to_update = set()

            save_key = Utils.get_chunk_pos_by_world_pos(world_position)
            self.chunks_to_save.add(self.chunks[save_key])
            self.chunks[save_key].need_save = True

            chunk_key_to_update.add(
                Utils.get_chunk_pos_by_world_pos(world_position))
            chunk_key_to_update.add(
                Utils.get_chunk_pos_by_world_pos((world_position[0] + 1, world_position[1], world_position[2])))
            chunk_key_to_update.add(
                Utils.get_chunk_pos_by_world_pos((world_position[0] - 1, world_position[1], world_position[2])))
            chunk_key_to_update.add(
                Utils.get_chunk_pos_by_world_pos((world_position[0], world_position[1], world_position[2] + 1)))
            chunk_key_to_update.add(
                Utils.get_chunk_pos_by_world_pos((world_position[0], world_position[1], world_position[2] - 1)))
            chunk_key_to_update.add(
                Utils.get_chunk_pos_by_world_pos((world_position[0] + 1, world_position[1], world_position[2] + 1)))
            chunk_key_to_update.add(
                Utils.get_chunk_pos_by_world_pos((world_position[0] - 1, world_position[1], world_position[2] - 1)))
            chunk_key_to_update.add(
                Utils.get_chunk_pos_by_world_pos((world_position[0] - 1, world_position[1], world_position[2] + 1)))
            chunk_key_to_update.add(
                Utils.get_chunk_pos_by_world_pos((world_position[0] + 1, world_position[1], world_position[2] - 1)))
            for chunk_key in chunk_key_to_update:
                if chunk_key in self.chunks:
                    self.chunks[chunk_key].generate_mesh()
            return True
        else:
            print("fail to put a block!")
            return False

    def save_world(self):
        for chunk in self.chunks_to_save:
            chunk.save()
        self.chunks_to_save.clear()
